package sqlstore

import (
	"fmt"
	"go-caipu/pkg/infra/zaplogger"
	"go-caipu/pkg/services/sqlstore/migrations"
	"go-caipu/pkg/services/sqlstore/sqlutil"
	"go-caipu/pkg/setting"
	"gorm.io/gorm/schema"
	"path/filepath"
	"sync"
	"time"

	"os"
	"strings"

	"github.com/glebarez/sqlite"
	"gorm.io/driver/mysql"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"

	"go-caipu/pkg/infra/fs"
	"go-caipu/pkg/infra/log"
	"go-caipu/pkg/infra/tracing"
	"go-caipu/pkg/registry"
	"go-caipu/pkg/services/sqlstore/migrator"

	jobModel "go-caipu/pkg/services/schedule/models"
)

var (
	//MODE 使用环境 development
	MODE = "development"
)

type SQLStore struct {
	Cfg                         *setting.Cfg
	engine                      *gorm.DB
	log                         log.Logger
	Dialect                     migrator.Dialect
	skipEnsureDefaultOrgAndUser bool //是否创建管理员用户
	migrations                  registry.DatabaseMigrator
	tracer                      tracing.Tracer
}

func ProvideService(cfg *setting.Cfg, migrations registry.DatabaseMigrator, tracer tracing.Tracer) (*gorm.DB, error) {
	s, err := newSQLStore(cfg, migrations, tracer)
	if err != nil {
		return nil, err
	}
	return s, nil
}
func newSQLStore(cfg *setting.Cfg, migrations registry.DatabaseMigrator, tracer tracing.Tracer) (*gorm.DB, error) {
	ss := &SQLStore{
		Cfg:        cfg,
		log:        log.New("sqlstore"),
		migrations: migrations,
		tracer:     tracer,
	}

	if err := ss.initEngine(); err != nil {
		return nil, fmt.Errorf("%v: %w", "failed to connect to database", err)
	}
	ss.Dialect = migrator.NewDialect(ss.engine)

	//init table
	if err := ss.Migrate(false); err != nil {
		return nil, err
	}
	ss.tracer = tracer
	return ss.engine, nil
}
func (ss *SQLStore) initEngine() error {
	if ss.engine != nil {
		ss.log.Debug("Already connected to database")
		return nil
	}
	dsn := ss.Cfg.DbCfg.DSN

	ss.log.Info("Connecting to DB", "dbType", ss.Cfg.DbCfg.DbType)

	if ss.Cfg.DbCfg.DbType == migrator.SQLite && strings.HasPrefix(dsn, "file:") &&
		!strings.HasPrefix(dsn, "file::memory:") {
		dbName := strings.Replace(dsn, "file:", "", -1)
		sqlitePath := filepath.Join(ss.Cfg.Application.HomePath, fmt.Sprintf("data/%s", dbName))
		//sqlitePath := filepath.Join(ss.Cfg.Application.DataPath, "data/caipu_db.db")
		exists, err := fs.Exists(sqlitePath)
		if err != nil {
			return fmt.Errorf("can't check for existence of %q: %w", ss.Cfg.Application.DataPath, err)
		}

		const perms = 0640
		if !exists {
			ss.log.Info("Creating SQLite database file", "path", sqlitePath)
			f, err := os.OpenFile(sqlitePath, os.O_CREATE|os.O_RDWR, perms)
			if err != nil {
				return fmt.Errorf("failed to create SQLite database file %q: %w", sqlitePath, err)
			}
			if err := f.Close(); err != nil {
				return fmt.Errorf("failed to create SQLite database file %q: %w", sqlitePath, err)
			}
		} else {
			fi, err := os.Lstat(sqlitePath)
			if err != nil {
				return fmt.Errorf("failed to stat SQLite database file %q: %w", sqlitePath, err)
			}
			m := fi.Mode() & os.ModePerm
			if m|perms != perms {
				ss.log.Warn("SQLite database file has broader permissions than it should",
					"path", sqlitePath, "mode", m, "expected", os.FileMode(perms))
			}
		}
		//设置sqlite dsn
		ss.Cfg.DbCfg.DSN = sqlitePath
	}
	var err error
	var dbConfig = &gorm.Config{
		NamingStrategy: schema.NamingStrategy{SingularTable: true}, // 使用单数表名
	}
	//设置日志(生产环境打印日志到文件）
	if ss.Cfg.Application.Mode != MODE {
		dbConfig.Logger = zaplogger.InitGormLogger(ss.Cfg)
	}

	var db *gorm.DB
	switch ss.Cfg.DbCfg.DbType {
	case migrator.SQLite:
		db, err = gorm.Open(sqlite.Open(ss.Cfg.DbCfg.DSN), dbConfig)
		db.Exec("PRAGMA journal_mode=WAL;") // 启用 WAL 模式(解决数据库锁的问题）
	case migrator.MySQL:
		db, err = gorm.Open(mysql.Open(ss.Cfg.DbCfg.DSN), dbConfig)
	case migrator.Postgres:
		db, err = gorm.Open(postgres.Open(ss.Cfg.DbCfg.DSN), dbConfig)
	default:
		return fmt.Errorf("unknown database type: %s", ss.Cfg.DbCfg.DbType)
	}
	if err != nil {
		return err
	}
	sqlDB, _ := db.DB()
	sqlDB.SetMaxIdleConns(ss.Cfg.DbCfg.MaxIdleConn)
	sqlDB.SetMaxOpenConns(ss.Cfg.DbCfg.MaxOpenConn)

	// 设置连接超时时间
	sqlDB.SetConnMaxLifetime(time.Minute * 3)

	ss.engine = db

	if ss.Cfg.Application.Mode == MODE {
		ss.engine = ss.engine.Debug()
	}
	return nil
}
func (ss *SQLStore) Migrate(isDatabaseLockingEnabled bool) error {
	if ss.Cfg.DbCfg.SkipMigrations {
		return nil
	}
	migrator := migrator.NewMigrator(ss.engine, ss.Cfg)
	ss.migrations.AddMigration(migrator)

	ss.engine.AutoMigrate(&jobModel.SysJob{})
	ss.engine.AutoMigrate(&jobModel.SysJobLog{})

	// For "mysql" only if migrationLocking feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
	//locking_attempt_timeout_sec = 0
	return migrator.Start(isDatabaseLockingEnabled, 0)

}

// ITestDB is an interface of arguments for testing db
type ITestDB interface {
	Helper()
	Fatalf(format string, args ...interface{})
	Logf(format string, args ...interface{})
	Log(args ...interface{})
}

// InitTestDBOpt contains options for InitTestDB.
type InitTestDBOpt struct {
	// EnsureDefaultOrgAndUser flags whether to ensure that default org and sysuser exist.
	EnsureDefaultOrgAndUser bool
	FeatureFlags            []string
}

func InitTestDB(t ITestDB) *gorm.DB {
	t.Helper()

	setCfg := setting.NewCfg()
	setCfg.Application.Mode = MODE //devopement mode
	engine, err := initTestDB(setCfg, &migrations.OSSMigrations{})
	if err != nil {
		t.Fatalf("failed to initialize sql store: %s", err)
	}
	return engine
}

var testSQLStoreMutex sync.Mutex
var testSQLStroe *gorm.DB

func initTestDB(testCfg *setting.Cfg, migration registry.DatabaseMigrator) (*gorm.DB, error) {
	testSQLStoreMutex.Lock()
	defer testSQLStoreMutex.Unlock()

	if testSQLStroe == nil {

		//配置测试参数
		dbType := migrator.SQLite

		// environment variable present for test db?
		if db, present := os.LookupEnv("CAIPU_TEST_DB"); present {
			dbType = db
		}

		switch dbType {
		case migrator.MySQL:
			testCfg.DbCfg.DSN = sqlutil.MySQLTestDB().ConnStr
		case migrator.MSSQL:
			testCfg.DbCfg.DSN = sqlutil.MSSQLTestDB().ConnStr
		case migrator.Postgres:
			testCfg.DbCfg.DSN = sqlutil.PostgresTestDB().ConnStr
		case migrator.SQLite:
			testCfg.DbCfg.DSN = sqlutil.SQLiteTestDB().ConnStr
		default:
			panic(fmt.Errorf("unknown database type: %s", dbType))
		}
		testCfg.DbCfg.DbType = dbType //default sqlite

		engine, err := newSQLStore(testCfg, migration, nil)
		if err != nil {
			return nil, err
		}
		return engine, nil
	}
	return testSQLStroe, nil
}
